package socket;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.*;

/**
 * 服务器
 */
public class Server {
    /*
        运行在服务端的ServerSocket主要用两个作用:
        1:向系统申请服务端口，客户端就是通过这个端口与服务器建立连接的
        2:监听服务端口，一口一个客户端建立连接就会创建一个Socket，使用这个Socket与客户端交互

        如果我们将Socket比喻为“电话”，则ServerSocket相当于是“总机”.
     */
    private ServerSocket server;
//    private PrintWriter[] allOut = {};
    private Collection<PrintWriter> allOut
        = Collections.synchronizedList(new ArrayList<>());//1.集合; 2.添加<E>泛型 //.synchronizedList并发安全并,互斥

    public Server() {
        try {
            /*
                实例化ServerSocket时需要指定服务端口，客户端就是通过这个端口与服务端建立连接的
                该端口不能与当前系统的其他应用程序申请的端口一致，
                否则会抛出地址已被占用的异常:
                java.net.BindException:address already in use : JVM
             */
            System.out.println("正在启动服务端。。。");
            server = new ServerSocket(8088);
            System.out.println("服务端启动完毕...");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 服务端
     */
    public void start() {
        try {
            /*
                ServerSocket提供的方法:
                Socket accept()
                该方法是一个阻塞方法，调用后会"卡住"，此时开始等待客户端的链接，一旦一个客户端
                建立链接时，该方法会立刻返回一个Socket。通过这个Socket就可以和该客户的交互了.
             */
            while (true) {
                System.out.println("等待客户端链接。。。");
                Socket socket = server.accept();//accept():接收
                System.out.println("一个客户端连接了...");

                /**启动一个线程来与该客户端交互.*/
                ClientHandLer handler = new ClientHandLer(socket);
                Thread t = new Thread(handler);
                t.start();
            }
//            System.out.println("等待客户端链接。。。");
//            Socket socket = server.accept();//accept():接收
//            System.out.println("一个客户端连接了...");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        Server server = new Server();
        server.start();
    }

    /**
     * ClientHandler 客户端处理器
     * 该类是一个线程任务，这就意味着每个这样的任务都是由一条线程执行的。
     * 该任务是负责与指定的客户端进行交互.
     */
    private class ClientHandLer implements Runnable {
        private Socket socket;
        private String host;//记录客户端的IP地址信息

        public ClientHandLer(Socket socket) {
            this.socket = socket;
            host = socket.getInetAddress().getHostAddress();//获取Inet地址，获取主机地址
        }

        public void run() {
            PrintWriter pw = null;
            try {
                InputStream in = socket.getInputStream();
                InputStreamReader isr = new InputStreamReader(in, "UTF-8");
                BufferedReader br = new BufferedReader(isr);

                OutputStream ot = socket.getOutputStream();
                OutputStreamWriter otr = new OutputStreamWriter(ot, "UTF-8");
                BufferedWriter bw = new BufferedWriter(otr);
                pw = new PrintWriter(bw, true);
                //将该输出流存入共享数组allOut中
//                synchronized (allOut) {//内部类引用外部类的this，语法要求类名.this;类名.this.外部属性
                    allOut.add(pw);
                //广播该用户上线了
                sendMessage(host + "上线了，当前在线人数:" + allOut.size());
                /*读取客户端发送过来的一行字符串*/
                String message;
            /*
                客户端如果正常调用了socket.close()与服务端断开连接时，服务端这里读取操作的方法
                readLine会返回null，表示客户端断开了连接，流读到了末尾，正常停止循环。
                但是如果客户端强制关闭了客户端，服务端无法正确得知客户端的断开就会抛出异常：
                java.net.SocketException:connection reset.连接重置
             */
                while ((message = br.readLine()) != null) {
                    sendMessage(host + "说:" + message);
                }
            } catch (IOException e) {
                System.out.println(host+"强行下线!");
            } finally {
                    //处理客户端断开连接的操作
                    allOut.remove(pw);
                System.out.println(allOut.size());
                //广播该用户下线了
                sendMessage(host + "下线了，当前在线人数:" + allOut.size());
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        public void sendMessage(String message) {
            System.out.println(message);
            allOut.forEach(pw->pw.println(message) );//遍历,且互斥
        }
    }
}
